TypeScript在类型检查方面非常强大,但有时,当某些类型是其他类型的子集且你又需要为它们定义类型检查时,TypeScript就会变得冗余。
举个例子,假设有两种响应类型:
xxxxxxxxxx131UserProfileResponse2interface UserProfileResponse {3 id: number;4 name: string;5 email: string;6 phone: string;7 avatar: string;8}9LoginResponse10interface LoginResponse {11 id: number;12 name: string;13}我们可以为UserProfileResponse定义类型并为LoginResponse选择属性,而不是定义相同上下文LoginResponse和UserProfileResponse的类型。
xxxxxxxxxx11type LoginResponse = Pick<UserProfileResponse, "id" | "name">;下面我们将介绍一些可以帮助编写更佳代码的实用程序函数。
构建一个类型,定义其属性全是小写。然后使用Uppercase方法将其全部转化为大写。
xxxxxxxxxx71type Role = "admin" | "user" | "guest";23// 不好的做法 💩4type UppercaseRole = "ADMIN" | "USER" | "GUEST";56// 正确的做法 ✅7type UppercaseRole = Uppercase<Role>; // "ADMIN" | "USER" | "GUEST"与前面一个例子相反,先构建一个属性全是大写的类型,然后使用Lowercase方法将其全部转化为小写。
xxxxxxxxxx71type Role = "ADMIN" | "USER" | "GUEST";23// 不好的做法 💩4type LowercaseRole = "admin" | "user" | "guest";56// 正确的做法 ✅7type LowercaseRole = Lowercase<Role>; // "admin" | "user" | "guest"将所有属性的首字母大写。
xxxxxxxxxx71type Role = "admin" | "user" | "guest";23// 不好的做法 💩4type CapitalizeRole = "Admin" | "User" | "Guest";56// 正确的做法 ✅7type CapitalizeRole = Capitalize<Role>; // "Admin" | "User" | "Guest"与Capitalize相反,将所有属性取消首字母大写。
xxxxxxxxxx71type Role = "Admin" | "User" | "Guest";23// 不好的做法 💩4type UncapitalizeRole = "admin" | "user" | "guest";56// 正确的做法 ✅7type UncapitalizeRole = Uncapitalize<Role>; // "admin" | "user" | "guest"将定义的接口中的属性设置为可选属性。
xxxxxxxxxx151interface User {2 name: string;3 age: number;4 password: string;5}67// 不好的做法 💩8interface PartialUser {9 name?: string;10 age?: number;11 password?: string;12}1314// 正确的做法 ✅15type PartialUser = Partial<User>;与Partial相反,Required可以将接口中的可选属性设置为必需。
xxxxxxxxxx151interface User {2 name?: string;3 age?: number;4 password?: string;5}67// 不好的做法 💩8interface RequiredUser {9 name: string;10 age: number;11 password: string;12}1314// 正确的做法 ✅15type RequiredUser = Required<User>;将属性设置为只读。
xxxxxxxxxx121interface User {2 role: string;3}45// 不好的做法 💩6const user: User = { role: "ADMIN" };7user.role = "USER";89// 正确的做法 ✅10type ReadonlyUser = Readonly<User>;11const user: ReadonlyUser = { role: "ADMIN" };12user.role = "USER"; // 将属性'role'设置为只读后,再次对'role'设值就会报错构造一个对象类型,其属性key是Keys,属性value是Tpye。被用于映射一个类型的属性到另一个类型。
xxxxxxxxxx121interface Address {2 street: string;3 pin: number;4}56interface Addresses {7 home: Address;8 office: Address;9}1011// Alternative ✅12type AddressesRecord = Record<"home" | "office", Address>;就是从一个复合类型中,取出几个想要的类型的组合,例如:
xxxxxxxxxx141interface User {2 name: string;3 age: number;4 password: string;5}67// 不好的做法 💩8interface UserPartial {9 name: string;10 age: number;11}1213// 正确的做法 ✅14type UserPartial = Pick<User, "name" | "age">;Omit是TypeScript3.5新增的一个辅助类型,它的作用主要是:以一个类型为基础支持剔除某些属性,然后返回一个新类型。
xxxxxxxxxx141interface User {2 name: string;3 age: number;4 password: string;5}67// 不好的做法 💩8interface UserPartial {9 name: string;10 age: number;11}1213// 正确的做法 ✅14type UserPartial = Omit<User, "password">;将类型中其中一些属性排除,并创建排除属性后的新类型。
xxxxxxxxxx71type Role = "ADMIN" | "USER" | "GUEST";23// 不好的做法 💩4type NonAdminRole = "USER" | "GUEST";56// 正确的做法 ✅7type NonAdmin = Exclude<Role, "ADMIN">; // "USER" | "GUEST"它通过从可分配给联合的类型中提取所有联合成员来创建新类型。
xxxxxxxxxx71type Role = "ADMIN" | "USER" | "GUEST";23// 不好的做法 💩4type AdminRole = "ADMIN";56// 正确的做法 ✅7type Admin = Extract<Role, "ADMIN">; // "ADMIN"通过从类型中排除null和undefined来创建新类型。
xxxxxxxxxx71type Role = "ADMIN" | "USER" | null;23// 不好的做法 💩4type NonNullableRole = "ADMIN" | "USER";56// 正确的做法 ✅7type NonNullableRole = NonNullable<Role>; // "ADMIN" | "USER"
例子很简单,但是必须把简单的例子记住,这就不简单了。
keyof作用:从一个对象类型中提取所有键名,组成一个字符串字面量联合类型。
例子:
xxxxxxxxxx11type UserKey = keyof User; // 'id' | 'name' | 'email'→ 说明:UserKey 只能是 User 这个类型里真正存在的字段名,防止写错属性名(比如写成 "names" 会报错)。
xxxxxxxxxx11function getProp<T, K extends keyof T>(obj: T, key: K): T[K]→ 说明:约束第二个参数 key 必须是 T 真正存在的键,保证运行时 obj[key] 一定能取到值且类型安全。
[](索引访问类型)作用:通过键(可以是字面量或联合类型)从对象类型中取出对应属性的类型。
例子:
xxxxxxxxxx11type PrimaryColor = Colors["primary"]; // "#007bff"→ 说明:精确地取出 primary 这个具体键对应的类型(这里是字面量类型 "#007bff"),而不是更宽泛的 string。
xxxxxxxxxx11type PropType = User[keyof User]; // number | string | boolean→ 说明:表示“User 对象所有可能的值的类型集合”,常用来描述“任意字段的值”。
extends ? :(条件类型)作用:像三元表达式一样,根据类型是否满足某个条件来选择不同的结果类型。
例子:
好的,我明白你觉得之前的例子太抽象、太“类型编程”了,看起来像谜语一样难懂。
我现在重新给你举 extends 的例子,这次全部用最简单、最日常的场景,像写普通代码一样解释清楚。每个例子都告诉你:
extends 到底在起什么作用(用大白话)extends 会怎样(为什么需要它)例子1:最简单、最常见的 —— “这个参数必须有 length 属性”
xxxxxxxxxx71function printLength<T extends { length: number }>(thing: T) {2 console.log(thing.length); // 这里放心用 length,不会报错3}45printLength("hello"); // OK,字符串有 length6printLength([1, 2, 3]); // OK,数组有 length7printLength(123); // 报错!数字没有 lengthextends 在干什么?
它在告诉 TypeScript:“T 这个类型必须长得像有 length 属性的样子”,不然不让传进来。
没有 extends { length: number },你传数字进来时,thing.length 会直接报错(因为 number 没 length)。
例子2:取对象属性的安全函数(超级常用)
xxxxxxxxxx91function getValue<T, K extends keyof T>(obj: T, key: K) {2 return obj[key]; // 类型安全,返回值是 obj[key] 的真实类型3}45const person = { name: "小明", age: 18, isStudent: true };67const name = getValue(person, "name"); // string8const age = getValue(person, "age"); // number9getValue(person, "height"); // 报错!没有 height 这个属性extends 在干什么?K extends keyof T 意思是:key 必须是 person 真正存在的属性名。
如果没有这个 extends,你写 getValue(person, "随便乱写") 也不会报错,但运行时 obj["随便乱写"] 就是 undefined,很危险。
例子3:接口继承(写组件 props 时最常见)
xxxxxxxxxx131interface ButtonProps {2 text: string;3 onClick: () => void;4}56interface FancyButtonProps extends ButtonProps {7 color: "red" | "blue";8 size: "small" | "large";9}1011function FancyButton(props: FancyButtonProps) {12 // 既有 text、onClick,也有 color、size13}extends 在干什么?
让 FancyButtonProps 自动拥有 ButtonProps 里所有的属性,再额外加自己的。
相当于 FancyButtonProps = { text, onClick, color, size },不用重复写。
例子4:判断是不是字符串(最基础的条件类型)
xxxxxxxxxx41type Check<T> = T extends string ? "这是字符串" : "不是字符串";23type Test1 = Check<"hello">; // "这是字符串"4type Test2 = Check<999>; // "不是字符串"extends 在干什么? 像 if 判断:如果 T 是 string,就选左边;否则选右边。 这就是“带 ? :”的经典用法。
例子5:去掉函数类型(过滤掉函数)
xxxxxxxxxx41type OnlyData<T> = T extends Function ? never : T;23type Mixed = string | number | (() => void) | boolean;4type Clean = OnlyData<Mixed>; // string | number | boolean (函数被踢掉了)extends 在干什么? 检查 T 是不是函数:如果是,就返回 never(相当于删除);不是就保留。
总结:你最常遇到的两种 extends(记住这两种就够用了)
不带 ? : 的(约束)
<T extends XXX> 或 interface Y extends X 带 ? : 的(条件判断)
T extends XXX ? 是 : 否 infer(类型推断)作用:在条件类型中“猜”出(捕获)某个位置的未知类型,把它绑定到一个变量上供后面使用。
例子:
xxxxxxxxxx11type ElementType<T> = T extends (infer U)[] ? U : never;→ 说明:infer U 让 TypeScript 自动推断出数组里元素的真实类型(比如 number[] → infer U 得到 number),然后把 U 作为结果返回。
xxxxxxxxxx11type MyReturnType<T> = T extends (args: any[]) => infer R ? R : never;→ 说明:捕获函数返回值的类型(R),实现类似官方 ReturnType 的功能。
in(映射类型中的遍历)作用:遍历一个联合类型(通常是 keyof 出来的),为每个成员生成一个属性。
例子:
xxxxxxxxxx11type StatusFlags = { [K in Keys]: boolean };→ 说明:把 Keys 里的每个字符串('loading'|'success'|'error')都变成一个属性名,并且每个属性值都是 boolean 类型,快速生成一组同构的布尔字段。
`xxx${T}yyy`作用:在类型层面进行字符串拼接、组合、转换,生成新的字符串字面量联合类型。
例子:
xxxxxxxxxx11type HandlerName = `on${Capitalize<Event>}`;→ 说明:把 Event 里的每个值(click → Click)首字母大写后拼接上 "on",生成规范的事件处理器名称类型,用于类型安全的字符串匹配。
satisfies(TS 4.9+)作用:对值进行类型检查(必须满足某个类型),但不收窄/丢失原有的更精确的字面量类型。
例子:
xxxxxxxxxx11const theme = { colors: { primary: "#0070f3", } } satisfies { colors: Record<string, string>; };→ 说明:强制要求 theme 必须符合后面的形状(有 colors 对象等),但保留了 "#0070f3" 这样的具体字面量类型,而不是退化为宽泛的 string。
不要将satisfies和extends搞混了。虽然中文翻译都好像是“是否满足”,但作用是完全不同的。它们根本不是同一类东西,作用的层面和目的完全不同。简单一句话总结:
- extends:类型层面的关系定义和约束(在类型系统里说“A 是 B 的子类型”或“必须符合 B”)
- satisfies:值层面的类型校验(在写具体值/对象/表达式时,说“这个值必须能赋值给某个类型,但别把它的具体类型变宽/变模糊”)
层级不同:一个在“类型世界”,一个在“值世界”
xxxxxxxxxx71// extends → 只出现在类型定义/泛型约束里(类型世界)2type Animal = { eat(): void };3interface Dog extends Animal { bark(): void } // Dog “继承/扩展” Animal(类型关系)45function fn<T extends { name: string }>(obj: T) { // 约束 T 必须有 name6console.log(obj.name);7}xxxxxxxxxx111// satisfies → 只出现在具体值/表达式后面(值世界)2const config = {3api: "https://example.com",4timeout: 5000 as const, // 保持 5000 这个字面量5retry: true6} satisfies {7api: string;8timeout: number;9retry: boolean;10// 可选:可以少写点属性,但不能多写或类型错11};
- extends:你在定义类型时用,告诉 TypeScript “这个类型必须长这样或包含那些东西”。
- satisfies:你在写 JavaScript 值时用,告诉 TypeScript “检查一下这个对象/值符不符合要求,但别把它的类型变宽”。
is(类型谓词 / 类型守卫)作用:告诉 TypeScript 在某个条件为 true 时,变量的类型可以被收窄成更具体的类型。
例子:
xxxxxxxxxx21function isString(value: unknown): value is string { }2if (isString(input)) { input.toUpperCase() }→ 说明:在 if 块内部,input 被 TypeScript 自动收窄为 string,从而允许调用字符串专属的方法,而不会报错。